第44章 线性表和链表
线性表是计算机科学中最基础、最常用的数据结构之一,它是由n个具有相同特性的数据元素组成的有限序列。
44.1 线性表的基本概念
44.1.1 定义
线性表是由个数据元素组成的有限序列,其中:
- 为线性表的长度,时称为空表。
- 数据元素可以是int、char等基础类型,也可以是结构体,所有元素类型必须统一。
- 元素线性关系:除首元素无前驱、尾元素无后继外,其余元素有唯一直接前驱、唯一直接后继。
44.1.2 线性表基础操作
- 初始化:创建空线性表
- 判空:判断长度是否为0
- 求长度:返回当前元素总数
- 获取指定下标元素
- 按值查找,返回位置(不存在返回-1)
- 指定位置插入元素
- 指定位置删除元素
- 清空所有元素
44.2 顺序表(数组实现的顺序存储)
44.2.1 存储结构
连续内存存放元素,同时记录当前长度、最大容量。
#define MAX_SIZE 100
typedef int ElemType;
typedef struct{
ElemType data[MAX_SIZE];
int length; // 当前有效元素个数
} SeqList;
44.2.2 核心操作实现
初始化顺序表
void initList(SeqList &L){
L.length = 0;
}
插入元素(第i个位置插入,下标逻辑1开始)
bool insertList(SeqList &L, int i, ElemType e){
if(i < 1 || i > L.length + 1) return false;
if(L.length >= MAX_SIZE) return false;
// 后移元素腾出位置
for(int j = L.length; j >= i; j--){
L.data[j] = L.data[j-1];
}
L.data[i-1] = e;
L.length++;
return true;
}
删除元素
bool deleteList(SeqList &L, int i, ElemType &e){
if(i < 1 || i > L.length) return false;
e = L.data[i-1];
// 元素前移覆盖
for(int j = i; j < L.length; j++){
L.data[j-1] = L.data[j];
}
L.length--;
return true;
}
按值查找
int locateElem(SeqList L, ElemType e){
for(int i = 0; i < L.length; i++){
if(L.data[i] == e){
return i + 1; // 返回1号位序
}
}
return -1;
}
44.2.3 顺序表优缺点
- 优点:随机访问,存储无额外指针开销
- 缺点:插入删除需要大量移动元素;容量固定易溢出/浪费内存
44.2.4 适用场景
查询多、增删少,元素数量固定的场景。
44.3 链表(链式存储)
链表不占用连续内存,依靠指针连接分散节点,每个节点包含数据域与指针域。
44.3.1 单链表
每个节点仅存后继指针,带表头节点简化边界逻辑。
typedef int ElemType;
typedef struct LNode{
ElemType data;
struct LNode *next;
} LNode, *LinkList;
初始化空链表(头节点)
void initList(LinkList &L){
L = new LNode;
L->next = nullptr;
}
头插法建表
void createListHead(LinkList &L, int n){
L = new LNode;
L->next = nullptr;
for(int i = 0; i < n; i++){
LNode *p = new LNode;
cin >> p->data;
p->next = L->next;
L->next = p;
}
}
尾插法建表
void createListTail(LinkList &L, int n) {
L = new LNode;
L->next = nullptr;
LNode *r = L; // 尾指针
for (int i = 0; i < n; i++){
LNode *p = new LNode;
cin >> p->data;
p->next = nullptr;
r->next = p;
r = p;
}
}
指定位置插入
bool insertList(LinkList &L, int i, ElemType e) {
LNode *p = L;
int j = 0;
while (p != nullptr && j < i-1){
p = p->next;
j++;
}
if (p == nullptr) return false;
LNode *s = new LNode;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
指定位置删除
bool deleteList(LinkList &L, int i, ElemType &e){
LNode *p = L;
int j = 0;
while (p != nullptr && j < i-1){
p = p->next;
j++;
}
if (p == nullptr || p->next == nullptr) return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
delete q;
return true;
}
按值查找节点
LNode* locateElem(LinkList L, ElemType e){
LNode *p = L->next;
while (p != nullptr && p->data != e){
p = p->next;
}
return p;
}
44.3.2 双向链表
节点同时存前驱、后继指针,可双向遍历
typedef struct DNode {
ElemType data;
struct DNode *prior;
struct DNode *next;
} DNode, *DLinkList;
特点:访问前驱;插入删除需要修改两个指针,代码更复杂。
44.3.3 循环单链表
尾节点next指向头节点,形成环形结构;适合约瑟夫环等环形问题。
struct Node {
int data;
Node* next;
};
44.3.4 链表通用优缺点
- 优点:动态分配内存,插入删除仅改指针(找到前驱前提下)
- 缺点:不支持随机访问,查找需遍历;每个节点额外存指针,内存开销更大
44.4 顺序表与链表对比
| 特性 | 顺序表 | 单链表 |
|---|---|---|
| 内存分布 | 连续 | 分散 |
| 随机访问 | ||
| 中间增删 | (找到前驱) | |
| 内存开销 | 仅存储数据 | 数据+指针 |
| 扩容 | 固定容量,需整体复制 | 按需分配 |
44.5 三类链表对比
| 类型 | 节点指针 | 尾节点指向 | 访问前驱 |
|---|---|---|---|
| 单链表 | 仅next | nullptr | |
| 双向链表 | prior+next | nullptr | |
| 循环单链表 | 仅next | 头节点 |
44.6 线性表应用场景
- 顺序表:数组、vector、需要频繁下标查询
- 单链表:任务队列、临时动态数据
- 双向链表:浏览器前进后退、LRU缓存
- 循环链表:约瑟夫问题、环形缓冲队列